#!/usr/bin/env python
#-*- coding:utf-8 -*-


try:
	import pygtk
	pygtk.require("2.0")
except:
	pass
try:
	import gtk
	import gtk.glade
except:
	sys.exit(1)

import gobject, pynotify, re, htmlentitydefs

class PyChatGTK(object):
	'''A graphical interface to the unofficial IED chat service'''
	
	gladefile = 'pychat.glade' # The glade file for the interface
	
	window = None # The main window
	wtree = None # The widget tree for the current interface
	
	users_store, rooms_store, avatars_store, user_status_store = 4 * [None] # gtk.ListStore instances
	users_store_refs, rooms_store_refs = {}, {}
	
	show_notifications = False # Boolean that tells if we show notifications or not
	
	chat = None # The IEDChat instance linked with this
	
	def __init__(self):
		'''Loads the glade file and initiate some containers'''
		
		self.wtree = gtk.glade.XML(self.gladefile)
		self.window = self.wtree.get_widget('mainWindow')
		
		pynotify.init("Chat Info") # We initialise pynotify (I'm not sure why...)
		
		map(lambda f: f(), [self.init_rooms_list, self.init_users_list, self.init_avatars_list, self.init_user_status_list]) # Initialise the different gtk.ListStore
		
		# The tray icon
		self.tray_icon = gtk.status_icon_new_from_stock(gtk.STOCK_UNDERLINE)
		self.tray_icon.connect('activate', self.tray_icon_activated_cb)
		
		self.init_text_buffer_tags()
		
		self.init_signals()
	
	def init_signals(self):
		'''Connects all the signals to the appropriate callbacks'''
		dic = {
			'on_mainWindow_destroy': self.quit,
			'on_send_message': self.send_message_cb,
			'on_change_status': self.change_status_cb,
			'on_change_avatar': self.change_avatar_cb,
			'on_room_select_changed': self.change_room_cb,
			'on_toggle_notify': self.toggle_notify,
			'on_mainWindow_focus': self.main_window_focus_cb
		}
		self.wtree.signal_autoconnect(dic)
	
	def init_rooms_list(self):
		'''Creates the treeview for the room list. Also associates the listStore with the room_select combobox'''
		self.rooms_store = gtk.ListStore(object)
		self.wtree.get_widget('roomList').set_model(self.rooms_store)
		
		cell_id = gtk.CellRendererText()
		col_id = gtk.TreeViewColumn('ID', cell_id)
		col_id.set_cell_data_func(cell_id, self.set_cell_text_to_object_attribute, 'id')
		
		cell_name = gtk.CellRendererText()
		col_name = gtk.TreeViewColumn('Name', cell_name)
		col_name.set_cell_data_func(cell_name, self.set_cell_text_to_object_attribute, 'name')
		
		map(lambda x: self.wtree.get_widget('roomList').append_column(x), [col_id, col_name])
		
		combobox = self.wtree.get_widget('room_select')
		cell = gtk.CellRendererText()
		combobox.set_model(self.rooms_store)
		combobox.pack_start(cell)
		#combobox.add_attribute(cell, 'text', 0)
		combobox.set_cell_data_func(cell, self.set_cell_text_to_object_attribute, 'name')
		
		return
	
	def init_users_list(self):
		'''Creates the treeview for the user list'''
		self.users_store = gtk.ListStore(object)
		self.wtree.get_widget('userList').set_model(self.users_store)
		
		# The first column : Display the id
		cell_id = gtk.CellRendererText()
		col_id = gtk.TreeViewColumn('ID', cell_id)
		col_id.set_cell_data_func(cell_id, self.set_cell_text_to_object_attribute, 'id')
		
		# The second column : display the avatar and the name
		col_name = gtk.TreeViewColumn('Name')
		cell_name = gtk.CellRendererText() # the part of the cell rendering the name+status
		cell_avatar = gtk.CellRendererPixbuf() # the part rendering the avatar
		col_name.pack_start(cell_avatar, False)
		col_name.pack_start(cell_name, True)
		col_name.set_cell_data_func(cell_name, self.set_cell_text_to_user_name_and_status)
		col_name.set_cell_data_func(cell_avatar, self.set_cell_pixbuf_to_user_room_avatar)
		col_name.set_expand(True)
		
		# The third column: display the current chatroom
		cell_room = gtk.CellRendererText()
		col_room = gtk.TreeViewColumn('Room', cell_room)
		col_room.set_cell_data_func(cell_room, self.set_cell_text_to_object_attribute, 'room')
		col_room.set_expand(True)
		
		map(lambda x: self.wtree.get_widget('userList').append_column(x), [col_id, col_name, col_room])
		
		return
	
	def init_avatars_list(self):
		'''Creates the treeview for the smileys list'''
		self.avatars_store = gtk.ListStore(str)
		combobox = self.wtree.get_widget('avatar_select')
		combobox.set_model(self.avatars_store)
		cell = gtk.CellRendererText()
		combobox.pack_start(cell)
		combobox.add_attribute(cell, 'text', 0)
		
		return
		
	
	def init_user_status_list(self):
		'''Creates the treeview for the user status list'''
		self.user_status_store = gtk.ListStore(int, str)
		combobox = self.wtree.get_widget('status_select')
		combobox.set_model(self.user_status_store)
		cell = gtk.CellRendererText()
		combobox.pack_start(cell)
		combobox.add_attribute(cell, 'text', 1)
		
		return
	
	def init_text_buffer_tags(self):
		'''Creates the tag table for the chat window text buffer'''
		
		# The "username" tag is used to display the name of a poster before a message
		user_tag = gtk.TextTag('username')
		user_tag.set_properties(weight=700) # bold
		self.wtree.get_widget('chatPanel').get_buffer().get_tag_table().add(user_tag)
	
	def add_user(self, user):
		'''Adds the given user and notifies if necessary'''
		iter = self.users_store.append([user])
		self.users_store_refs[user.id] = gtk.TreeRowReference(self.users_store, self.users_store.get_path(iter))
		self.wtree.get_widget('userList').queue_draw()
		self.notify('New user', 'User <b>%s</b> has just arrived' % user.name)
	
	def delete_user(self, user):
		'''Deletes the given user and notifies if necessary'''
		tree_ref = self.users_store_refs.get(user.id)
		self.users_store.remove(self.users_store.get_iter(tree_ref.get_path()))
		self.notify('User gone', 'User <b>%s</b> has just left' % user.name)
	
	def add_room(self, room):
		'''Adds the given room'''
		iter = self.rooms_store.append([room])
		self.rooms_store_refs[room.id] = gtk.TreeRowReference(self.rooms_store, self.rooms_store.get_path(iter))
		self.wtree.get_widget('roomList').queue_draw()
	
	def delete_room(self, room):
		'''Deletes the given room'''
		tree_ref = self.rooms_store_refs.get(room.id)
		self.rooms_store.remove(self.rooms_store.get_iter(tree_ref.get_path()))
	
	def add_avatar(self, avatar):
		'''Adds the given smiley'''
		self.avatars_store.append([avatar])
		self.wtree.get_widget('avatar_select').queue_draw()
	
	def add_avatars(self, avatars):
		'''Shortcut to add a list of avatars. It's also more efficient since we only force the redraw at the end'''
		for avatar in avatars:
			self.avatars_store.append([avatar])
		self.wtree.get_widget('avatar_select').queue_draw()
	
	def delete_avatar(self, avatar):
		'''Deletes the given avatar'''
		pass
	
	def add_user_status(self, value, text):
		'''Adds the given user status'''
		self.user_status_store.append([value, text])
		self.wtree.get_widget('status_select').queue_draw()
	
	def add_user_statuses(self, statuses):
		'''Shortcut method to add a list of statuses'''
		for value, text in statuses:
			self.user_status_store.append([value, text])
		self.wtree.get_widget('status_select').queue_draw()
	
	def delete_user_status(self, value, text):
		'''Deletes the given user status'''
		pass
	
	def room_avatar_changed(self, user):
		'''Changes the room avatar for the given user'''
		self.wtree.get_widget('userList').queue_draw()
	
	def main_avatar_changed(self, user):
		'''Changes the main avatar for the given user'''
		self.wtree.get_widget('userList').queue_draw()
	
	def user_status_changed(self, user):
		'''Changes the status for the given user'''
		self.wtree.get_widget('userList').queue_draw()
	
	def user_changed_room(self, user):
		'''Change the room of a user'''
		self.wtree.get_widget('userList').queue_draw()
	
	def message_received(self, sender, message):
		'''Appends the message and notifies if necessary'''
		
		tview = self.wtree.get_widget('chatPanel')
		buf = tview.get_buffer()
		buf.insert_with_tags_by_name(buf.get_end_iter(), '%s :' % sender.name, 'username')
		buf.insert(buf.get_end_iter(), self.message_postfilter(message, False) + '\n')
		tview.scroll_to_iter(buf.get_end_iter(), 0)
		self.notify('%s said' % sender.name, self.message_postfilter(message, True))
	
	def append_message(self, message):
		'''Appends a message to the bottom of the textview'''
		view = self.wtree.get_widget('chatPanel')
		buf = tview.get_buffer()
		buf.insert(buf.get_end_iter(), msg)
		tview.scroll_to_iter(buf.get_end_iter(), 0)
	
	def notify(self, title, message):
		'''Use libnotify to pop up a message'''
		if self.show_notifications:
			self.force_notify(title, message)
	
	def force_notify(self, title, message):
		'''Force a notification, no matter the status of the show_notify attribute'''
		self.tray_icon.set_blinking(True) # Blinks the tray icon
		self.window.set_urgency_hint(True)
		notif = pynotify.Notification(title, message)
		notif.attach_to_status_icon(self.tray_icon)
		notif.show()
	
	def set_cell_text_to_object_attribute(self, column, cell, model, iter, attribute_name):
		'''CellRenderer callback that set the cell text attribute to the value of the given property of the object in the given row'''
		o = model.get_value(iter, 0) # the object corresponding to the given iter
		if o:
			cell.set_property('text', str(o.__dict__.get(attribute_name, '')))
		return
	
	def set_cell_pixbuf_to_user_room_avatar(self, column, cell, model, iter):
		'''CellRenderer callback that sets the cell pixbuf attribute to a pixbuf containing the user avatar'''
		u = model.get_value(iter, 0)
		if u:
			avatar = u.room_avatar or ':noAvatar:'
			cell.set_property('pixbuf', gtk.gdk.pixbuf_new_from_file('smileys/smi_%s.gif' % avatar[1:-1]))
		return
	
	def set_cell_text_to_user_name_and_status(self, column, cell, model, iter):
		'''CellRenderer callback that sets the cell text attribute to the value of the IEDUser.name_and_status() method'''
		u = model.get_value(iter, 0)
		if u:
			cell.set_property('text', u.name_and_status())
		return
	
	def quit(self, window):
		'''Disconnects from the chat and exits the program'''
		print 'Good Bike !'
		self.chat.disconnect()
		gtk.main_quit()
	
	def send_message_cb(self, widget):
		'''Sends a message to the chat instance and clears the input field'''
		field = self.wtree.get_widget('sendField')
		msg = field.get_text()
		field.set_text('') # clears the input
		if msg: self.chat.send_message(self.message_prefilter(msg))
	
	def change_avatar_cb(self, cb_widget):
		'''Requests an avatar change to the chat instance'''
		new_avatar = cb_widget.get_model()[cb_widget.get_active()][0]
		self.chat.request_change_avatar(':%s:' % new_avatar)
	
	def toggle_notify(self, widget):
		'''Toggles the notification'''
		self.show_notifications = self.wtree.get_widget('toggle_notify').get_active()
	
	def change_status_cb(self, cb_widget):
		'''Requests a status change to the chat instance'''
		new_status = cb_widget.get_model()[cb_widget.get_active()][0]
		self.chat.request_change_status(new_status)
	
	def main_window_focus_cb(self, window, foobar):
		'''Stops tray icon blinking and resets normal urgency'''
		self.tray_icon.set_blinking(False)
		self.window.set_urgency_hint(False)
	
	def tray_icon_activated_cb(self, icon):
		'''Puts the window to the foreground'''
		self.window.present()
	
	def change_room_cb(self, cb_widget):
		'''Requests a room change to the chat instance'''
		new_room = cb_widget.get_model()[cb_widget.get_active()][0]
		self.chat.request_change_room(new_room)
	
	def message_prefilter(self, msg):
		'''A filter for outgoing messages.'''
		for badword, replace in self.chat.blocked_words.iteritems():
			msg = msg.replace(badword, replace)
		return msg
	
	def message_postfilter(self, msg, notify = False):
		'''A filter for incoming messages that are displayed in the main window.
		Additionally, a second parameters indicate if the message is intended to be displayed in a notification tooltip.'''
		#TODO: handle markup like <a>, <u>, <b>
		if not notify:
			return self.html_entities_decode(msg)
		else:
			return msg
	
	def html_entities_decode(self, text):
		'''Decodes html entities in a text string. Replaces them by their unicode equivalent.
		Code taken from http://effbot.org/zone/re-sub.htm#unescape-html'''
		def fixup(m):
			text = m.group(0)
			if text[:2] == "&#":
				# character reference
				try:
					if text[:3] == "&#x":
						return unichr(int(text[3:-1], 16))
					else:
						return unichr(int(text[2:-1]))
				except ValueError:
					pass
			else:
				# named entity
				try:
					text = unichr(htmlentitydefs.name2codepoint[text[1:-1]])
				except KeyError:
					pass
			return text # leave as is
		return re.sub("&#?\w+;", fixup, text)
